// Copyright (c) 2019 Presto Labs Pte. Ltd.
// Author: linchuan

#pragma once

#include <iostream>
#include <memory>
#include <optional>
#include <set>
#include <sstream>
#include <string>
#include <vector>

#include <boost/date_time/posix_time/posix_time.hpp>

#include "coin2/exchange/base/symbology/derivative_util.h"
#include "coin2/exchange/base/symbology/futures_product.h"
#include "coin2/exchange/prex_futures/symbology/currency.h"

namespace coin2::exchange::prex_futures::symbology {

using PrexFuturesCurrency = prex_futures::symbology::PrexFuturesCurrency;

class PrexFuturesProduct
    : public base::symbology::FuturesProduct<PrexFuturesCurrency, PrexFuturesProduct>,
      public base::symbology::ProductFromStrMixin<PrexFuturesProduct> {
 public:
  using FuturesProduct = base::symbology::FuturesProduct<PrexFuturesCurrency, PrexFuturesProduct>;
  using ContractType = base::symbology::ContractType;

 public:
  PrexFuturesProduct(
      const PrexFuturesCurrency& base,
      const PrexFuturesCurrency& quote,
      const std::string& native_product,
      const std::string& native_subscription_symbol,
      const ContractType contract_type,
      const int64_t expiry_timestamp,
      const int64_t resolve_timestamp)
      : FuturesProduct(
            base,
            quote,
            native_product,
            native_subscription_symbol,
            base::market::ExchangeType::Prex,
            contract_type,
            expiry_timestamp,
            resolve_timestamp) {}

 public:
  static std::unique_ptr<PrexFuturesProduct> FromNativeStrImpl(
      const std::string& native_product,
      const int64_t resolve_timestamp) {
    std::vector<std::string> out;
    // e.g. BTCUSD.PERP
    out = Split(native_product, ".");
    if (out.size() != 2) {
      return {};
    }

    int len = 0;
    std::string quote_str;
    if (StringEndsWith(out[0], "USDT")) {
      len = out[0].size() - 4;  // Assume quote in USDT.
      quote_str = std::string("USDT");
    } else if (StringEndsWith(out[0], "USD")) {
      len = out[0].size() - 3;  // Assume quote in USD.
      quote_str = std::string("USD");
    } else {
      CHECK(false);
    }

    const auto base =
        PrexFuturesCurrency::FromNativeStrNoThrow(native_product.substr(0, len), resolve_timestamp);

    if (!base) {
      return {};
    }

    const auto quote =
        PrexFuturesCurrency::FromNativeStrNoThrow(quote_str.c_str(), resolve_timestamp);

    std::string expiry_str = "PERPETUAL";

    std::string native_product_tmp;
    std::string native_subscription_symbol;
    ContractType contract_type;
    int64_t expiry_timestamp;
    bool success = CalculateParams(
        *base,
        *quote,
        expiry_str,
        resolve_timestamp,
        &native_product_tmp,
        &native_subscription_symbol,
        &contract_type,
        &expiry_timestamp);
    if (success) {
      DCHECK_EQ(native_product_tmp, native_product);
      return std::make_unique<PrexFuturesProduct>(
          *base,
          *quote,
          native_product,
          native_subscription_symbol,
          contract_type,
          expiry_timestamp,
          resolve_timestamp);
    } else {
      return {};
    }
  }

  static char MonthToCode(int month) {
    const char* month_code_arr = "FGHJKMNQUVXZ";
    return month_code_arr[month - 1];
  }

  static void GetMonthYearFromTimestamp(const int64_t& timestamp, int* year, int* month) {
    using namespace boost::posix_time;

    const auto dt = from_time_t(::ceil(timestamp / 1e9));
    *year = static_cast<int>(dt.date().year());
    *month = static_cast<int>(dt.date().month());
  }

  static bool CalculateParams(
      const PrexFuturesCurrency& base,
      const PrexFuturesCurrency& quote,
      const std::string& expiry_str,
      const int64_t resolve_timestamp,
      std::string* native_product,
      std::string* native_subscription_symbol,
      ContractType* contract_type,
      int64_t* expiry_timestamp) {
    using base::symbology::ContractType;
    using base::symbology::IsDateStr;
    using base::symbology::StringToContractType;
    using base::symbology::ToDateStr;

    if (IsDateStr(expiry_str)) {
      auto quarter_timestamp = FindExpiryQuarter(resolve_timestamp);
      auto quarter_date_str = ToDateStr(quarter_timestamp);
      auto next_quarter_timestamp = FindExpiryNextQuarter(resolve_timestamp);
      auto next_quarter_date_str = ToDateStr(next_quarter_timestamp);
      if (quarter_date_str == expiry_str) {
        *expiry_timestamp = quarter_timestamp;
      } else if (next_quarter_date_str == expiry_str) {
        *expiry_timestamp = next_quarter_timestamp;
      } else {
        return false;
      }
    } else {
      auto type = StringToContractType(expiry_str);
      if (type) {
        *contract_type = type.value();
        if (*contract_type == ContractType::PERPETUAL) {
          *expiry_timestamp = FuturesProduct::PERPETUAL_TIMESTAMP;
        } else if (*contract_type == ContractType::WEEK) {
          NOTREACHED();
        } else if (*contract_type == ContractType::QUARTER) {
          *expiry_timestamp = FindExpiryQuarter(resolve_timestamp);
        } else if (*contract_type == ContractType::NEXT_QUARTER) {
          *expiry_timestamp = FindExpiryNextQuarter(resolve_timestamp);
        } else {
          NOTREACHED();
        }
      } else {
        return false;
      }
    }

    std::stringstream ss;
    if (*contract_type == ContractType::PERPETUAL) {
      ss << base.native_currency() << quote.native_currency() << ".PERP";
    } else {
      using namespace boost::posix_time;

      auto expiry_date = from_time_t(::ceil(*expiry_timestamp / 1e9)).date();
      ss << base.native_currency() << MonthToCode(expiry_date.month())
         << std::to_string(expiry_date.year() % 100);
    }
    *native_product = ss.str();
    *native_subscription_symbol = *native_product;
    return true;
  }

  static int64_t FindExpiryQuarter(const int64_t resolve_timestamp) {
    int current_year;
    int current_month;
    GetMonthYearFromTimestamp(resolve_timestamp, &current_year, &current_month);
    auto expiry_month = current_month + (12 - current_month) % 3;

    using namespace boost::posix_time;
    using namespace boost::gregorian;
    using namespace coin2::exchange::base::symbology;

    auto possible_expiry_date =
        last_day_of_the_week_in_month(Friday, expiry_month).get_date(current_year);
    const auto td = microseconds(expiry_time_of_day_nanosec / 1000);
    int64_t possible_expiry_timestamp = DatetimeToTimestamp(ptime(possible_expiry_date, td));
    if (resolve_timestamp < possible_expiry_timestamp) {
      return possible_expiry_timestamp;
    } else {
      return FindExpiryQuarter(possible_expiry_timestamp + 75 * 24 * 3600 * 1e9);
    }
  }

  static int64_t FindExpiryNextQuarter(const int64_t resolve_timestamp) {
    int64_t quarter_expiry_timestamp = FindExpiryQuarter(resolve_timestamp);
    return FindExpiryQuarter(quarter_expiry_timestamp + 75 * 24 * 3600 * 1e9);
  }

  static bool CalculateContractType(
      const std::string& short_expiry_code,
      const int64_t resolve_timestamp,
      std::string* expiry_str) {
    using base::symbology::ContractType;

    int64_t quarter_expiry_timestamp = FindExpiryQuarter(resolve_timestamp);
    int quarter_month;
    int quarter_year;
    GetMonthYearFromTimestamp(quarter_expiry_timestamp, &quarter_year, &quarter_month);
    std::string calc_code = MonthToCode(quarter_month) + std::to_string(quarter_year % 100);
    if (calc_code == short_expiry_code) {
      *expiry_str = "QUARTER";
      return true;
    } else {
      int64_t next_quarter_expiry_timestamp =
          FindExpiryQuarter(quarter_expiry_timestamp + 75 * 24 * 3600 * 1e9);
      GetMonthYearFromTimestamp(next_quarter_expiry_timestamp, &quarter_year, &quarter_month);
      calc_code = MonthToCode(quarter_month) + std::to_string(quarter_year % 100);
      if (calc_code == short_expiry_code) {
        *expiry_str = "NEXT_QUARTER";
        return true;
      }
    }
    return false;
  }

  static const int64_t expiry_time_of_day_nanosec = 12 * 3600 * 1e9;
};

}  // namespace coin2::exchange::prex_futures::symbology
